#!/bin/sh
###############################################################################
#                                                                             #
# IPFire.org - A linux based firewall                                         #
# Copyright (C) 2007-2022  IPFire Team  <info@ipfire.org>                     #
#                                                                             #
# This program is free software: you can redistribute it and/or modify        #
# it under the terms of the GNU General Public License as published by        #
# the Free Software Foundation, either version 3 of the License, or           #
# (at your option) any later version.                                         #
#                                                                             #
# This program is distributed in the hope that it will be useful,             #
# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
# GNU General Public License for more details.                                #
#                                                                             #
# You should have received a copy of the GNU General Public License           #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
#                                                                             #
###############################################################################

. /etc/sysconfig/rc
. ${rc_functions}

# Cache any local zones for 60 seconds
LOCAL_TTL=60

# Load configuration
eval $(/usr/local/bin/readhash /var/ipfire/dns/settings)
eval $(/usr/local/bin/readhash /var/ipfire/ethernet/settings)

ip_address_revptr() {
	local addr=${1}

	local a1 a2 a3 a4
	IFS=. read -r a1 a2 a3 a4 <<< ${addr}

	echo "${a4}.${a3}.${a2}.${a1}.in-addr.arpa"
}

read_name_servers() {
	# Read name servers from ISP
	if [ "${USE_ISP_NAMESERVERS}" = "on" -a "${PROTO}" != "TLS" ]; then
		local i
		for i in 1 2; do
			echo "$(</var/run/dns${i})"
		done 2>/dev/null
	fi

	# Read configured name servers
	local id address tls_hostname enabled remark
	while IFS="," read -r id address tls_hostname enabled remark; do
		[ "${enabled}" != "enabled" ] && continue

		if [ "${PROTO}" = "TLS" ]; then
			if [ -n "${tls_hostname}" ]; then
				echo "${address}@853#${tls_hostname}"
			fi
		else
			echo "${address}"
		fi
	done < /var/ipfire/dns/servers
}

config_header() {
	echo "# This file is automatically generated and any changes"
	echo "# will be overwritten. DO NOT EDIT!"
	echo
}

write_hosts_conf() {
	(
		config_header

		# Make own hostname resolveable
		# 1.1.1.1 is reserved for unused green, skip this
		if [ -n "${GREEN_ADDRESS}" -a "${GREEN_ADDRESS}" != "1.1.1.1" ]; then
			echo "local-data: \"${HOSTNAME} ${LOCAL_TTL} IN A ${GREEN_ADDRESS}\""
		fi

		local address
		for address in ${GREEN_ADDRESS} ${BLUE_ADDRESS} ${ORANGE_ADDRESS}; do
			[ -n "${address}" ] || continue
			[ "${address}" = "1.1.1.1" ] && continue

			address=$(ip_address_revptr ${address})
			echo "local-data: \"${address} ${LOCAL_TTL} IN PTR ${HOSTNAME}\""
		done

		local enabled address hostname domainname generateptr

		# Find all unique domain names
		while IFS="," read -r enabled address hostname domainname generateptr; do
			[ "${enabled}" = "on" ] || continue

			# Skip empty domainnames
			[ "${domainname}" = "" ] && continue

			echo "local-zone: ${domainname} transparent"
		done < /var/ipfire/main/hosts | sort -u

		# Add all hosts
		while IFS="," read -r enabled address hostname domainname generateptr; do
			[ "${enabled}" = "on" ] || continue

			# Build FQDN
			local fqdn="${hostname}.${domainname}"
			echo "local-data: \"${fqdn} ${LOCAL_TTL} IN A ${address}\""

			# Skip reverse resolution if the address equals the GREEN address
			[ "${address}" = "${GREEN_ADDRESS}" ] && continue

			# Skip reverse resolution if user requested not to do so
			[ "${generateptr}" = "off" ] && continue

			# Add RDNS
			address=$(ip_address_revptr ${address})
			echo "local-data: \"${address} ${LOCAL_TTL} IN PTR ${fqdn}\""
		done < /var/ipfire/main/hosts
	) > /etc/unbound/hosts.conf
}

write_forward_conf() {
	(
		config_header

		# Enable strict QNAME minimisation
		if [ "${QNAME_MIN}" = "strict" ]; then
			echo "server:"
			echo "	qname-minimisation-strict: yes"
			echo
		fi

		# Force using TCP for upstream servers only
		if [ "${PROTO}" = "TCP" ]; then
			echo "# Force using TCP for upstream servers only"
			echo "server:"
			echo "	tcp-upstream: yes"
			echo
		fi

		local insecure_zones=""

		local enabled zone server servers remark disable_dnssec rest
		while IFS="," read -r enabled zone servers remark disable_dnssec rest; do
			# Line must be enabled.
			[ "${enabled}" = "on" ] || continue

			# Zones that end with .local are commonly used for internal
			# zones and therefore not signed
			case "${zone}" in
				*.local)
					insecure_zones="${insecure_zones} ${zone}"
					;;
				*)
					if [ "${disable_dnssec}" = "on" ]; then
						insecure_zones="${insecure_zones} ${zone}"
					fi
					;;
			esac

			echo "stub-zone:"
			echo "	name: ${zone}"
			for server in ${servers//|/ }; do
				if [[ ${server} =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
					echo "	stub-addr: ${server}"
				else
					echo "	stub-host: ${server}"
				fi
			done
			echo

			# Make all reverse lookup zones transparent
			case "${zone}" in
				*.in-addr.arpa)
					echo "server:"
					echo "	local-zone: \"${zone}\" transparent"
					echo
					;;
			esac
		done < /var/ipfire/dnsforward/config

		if [ -n "${insecure_zones}" ]; then
			echo "server:"

			for zone in ${insecure_zones}; do
				echo "	domain-insecure: ${zone}"
			done
		fi

		# Read name servers.
		nameservers=$(read_name_servers)

		# Only write forward zones if any nameservers are configured.
		#
		# Otherwise fall-back into recursor mode.
		if [ -n "${nameservers}" ]; then

			echo "forward-zone:"
			echo "	name: \".\""

			# Force using TLS only
			if [ "${PROTO}" = "TLS" ]; then
				echo "	forward-tls-upstream: yes"
			fi

			# Add upstream name servers
			local ns
			for ns in ${nameservers}; do
				echo "	forward-addr: ${ns}"
			done
		fi

	) > /etc/unbound/forward.conf
}

write_tuning_conf() {
	# https://www.unbound.net/documentation/howto_optimise.html

	# Determine amount of system memory
	local mem=$(get_memory_amount)

	# In the worst case scenario, unbound can use double the
	# amount of memory allocated to a cache due to malloc overhead

	# Even larger systems with more than 8GB of RAM
	if [ ${mem} -ge 8192 ]; then
		mem=1024

	# Extra large systems with more than 4GB of RAM
	elif [ ${mem} -ge 4096 ]; then
		mem=512

	# Large systems with more than 2GB of RAM
	elif [ ${mem} -ge 2048 ]; then
		mem=256

	# Medium systems with more than 1GB of RAM
	elif [ ${mem} -ge 1024 ]; then
		mem=128

	# Small systems with less than 256MB of RAM
	elif [ ${mem} -le 256 ]; then
		mem=16

	# Everything else
	else
		mem=64
	fi

	(
		config_header

		# Slice up the cache
		echo "rrset-cache-size: $(( ${mem} / 2 ))m"
		echo "msg-cache-size: $(( ${mem} / 4 ))m"
		echo "key-cache-size: $(( ${mem} / 4 ))m"

		# Increase parallel queries
		echo "outgoing-range: 8192"
		echo "num-queries-per-thread: 4096"

		# Use larger send/receive buffers
		echo "so-sndbuf: 4m"
		echo "so-rcvbuf: 4m"
	) > /etc/unbound/tuning.conf
}

get_memory_amount() {
	local key val unit

	while read -r key val unit; do
		case "${key}" in
			MemTotal:*)
				# Convert to MB
				echo "$(( ${val} / 1024 ))"
				break
				;;
		esac
	done < /proc/meminfo
}

fix_time_if_dns_fails() {
	# If DNS is working, everything is fine
	if resolve "0.ipfire.pool.ntp.org" &>/dev/null || \
	   resolve "1.ipfire.pool.ntp.org" &>/dev/null ; then
		return 0
	fi

	# Try to sync time with a known time server
	boot_mesg "DNS not functioning... Trying to sync time with time.ipfire.org (81.3.27.46)..."
	loadproc /usr/local/bin/settime 81.3.27.46
}

resolve() {
	local hostname="${1}"
	local found=1

	local answer
	for answer in $(dig +short A "${hostname}"); do
		# Filter out non-IP addresses
		if [[ ! "${answer}" =~ \.$ ]]; then
			found=0
			echo "${answer}"
		fi
	done

	return ${found}
}

# Sets up Safe Search for various search engines
update_safe_search() {
	local google_tlds=(
		google.ad
		google.ae
		google.al
		google.am
		google.as
		google.at
		google.az
		google.ba
		google.be
		google.bf
		google.bg
		google.bi
		google.bj
		google.bs
		google.bt
		google.by
		google.ca
		google.cat
		google.cd
		google.cf
		google.cg
		google.ch
		google.ci
		google.cl
		google.cm
		google.cn
		google.co.ao
		google.co.bw
		google.co.ck
		google.co.cr
		google.co.id
		google.co.il
		google.co.in
		google.co.jp
		google.co.ke
		google.co.kr
		google.co.ls
		google.com
		google.co.ma
		google.com.af
		google.com.ag
		google.com.ai
		google.com.ar
		google.com.au
		google.com.bd
		google.com.bh
		google.com.bn
		google.com.bo
		google.com.br
		google.com.bz
		google.com.co
		google.com.cu
		google.com.cy
		google.com.do
		google.com.ec
		google.com.eg
		google.com.et
		google.com.fj
		google.com.gh
		google.com.gi
		google.com.gt
		google.com.hk
		google.com.jm
		google.com.kh
		google.com.kw
		google.com.lb
		google.com.ly
		google.com.mm
		google.com.mt
		google.com.mx
		google.com.my
		google.com.na
		google.com.nf
		google.com.ng
		google.com.ni
		google.com.np
		google.com.om
		google.com.pa
		google.com.pe
		google.com.pg
		google.com.ph
		google.com.pk
		google.com.pr
		google.com.py
		google.com.qa
		google.com.sa
		google.com.sb
		google.com.sg
		google.com.sl
		google.com.sv
		google.com.tj
		google.com.tr
		google.com.tw
		google.com.ua
		google.com.uy
		google.com.vc
		google.com.vn
		google.co.mz
		google.co.nz
		google.co.th
		google.co.tz
		google.co.ug
		google.co.uk
		google.co.uz
		google.co.ve
		google.co.vi
		google.co.za
		google.co.zm
		google.co.zw
		google.cv
		google.cz
		google.de
		google.dj
		google.dk
		google.dm
		google.dz
		google.ee
		google.es
		google.fi
		google.fm
		google.fr
		google.ga
		google.ge
		google.gg
		google.gl
		google.gm
		google.gp
		google.gr
		google.gy
		google.hn
		google.hr
		google.ht
		google.hu
		google.ie
		google.im
		google.iq
		google.is
		google.it
		google.je
		google.jo
		google.kg
		google.ki
		google.kz
		google.la
		google.li
		google.lk
		google.lt
		google.lu
		google.lv
		google.md
		google.me
		google.mg
		google.mk
		google.ml
		google.mn
		google.ms
		google.mu
		google.mv
		google.mw
		google.ne
		google.nl
		google.no
		google.nr
		google.nu
		google.pl
		google.pn
		google.ps
		google.pt
		google.ro
		google.rs
		google.ru
		google.rw
		google.sc
		google.se
		google.sh
		google.si
		google.sk
		google.sm
		google.sn
		google.so
		google.sr
		google.st
		google.td
		google.tg
		google.tk
		google.tl
		google.tm
		google.tn
		google.to
		google.tt
		google.vg
		google.vu
		google.ws
	)

	# Cleanup previous settings
	unbound-control local_zone_remove "bing.com" >/dev/null
	unbound-control local_zone_remove "duckduckgo.com" >/dev/null
	unbound-control local_zone_remove "yandex.com" >/dev/null
	unbound-control local_zone_remove "yandex.ru" >/dev/null
	unbound-control local_zone_remove "youtube.com" >/dev/null

	local domain
	for domain in ${google_tlds[@]}; do
		unbound-control local_zone_remove "${domain}"
	done >/dev/null

	if [ "${ENABLE_SAFE_SEARCH}" = "on" ]; then
		# Bing
		unbound-control bing.com transparent >/dev/null
		for address in $(resolve "strict.bing.com"); do
			unbound-control local_data "www.bing.com ${LOCAL_TTL} IN A ${address}"
		done >/dev/null

		# DuckDuckGo
		unbound-control local_zone duckduckgo.com typetransparent >/dev/null
		for address in $(resolve "safe.duckduckgo.com"); do
			unbound-control local_data "duckduckgo.com ${LOCAL_TTL} IN A ${address}"
		done >/dev/null

		# Google
		local addresses="$(resolve "forcesafesearch.google.com")"
		for domain in ${google_tlds[@]}; do
			unbound-control local_zone "${domain}" transparent >/dev/null
			for address in ${addresses}; do
				unbound-control local_data "www.${domain} ${LOCAL_TTL} IN A ${address}"
			done >/dev/null
		done

		# Yandex
		for domain in yandex.com yandex.ru; do
			unbound-control local_zone "${domain}" typetransparent >/dev/null
			for address in $(resolve "familysearch.${domain}"); do
				unbound-control local_data "${domain} ${LOCAL_TTL} IN A ${address}"
			done >/dev/null
		done

		# YouTube
		if [ "${ENABLE_SAFE_SEARCH_YOUTUBE}" = "on" ]; then
			unbound-control local_zone youtube.com transparent >/dev/null
			for address in $(resolve "restrictmoderate.youtube.com"); do
				unbound-control local_data "www.youtube.com ${LOCAL_TTL} IN A ${address}"
			done >/dev/null
		fi
	fi

	return 0
}

case "$1" in
	start)
		# Print a nicer messagen when unbound is already running
		if pidofproc -s unbound; then
			statusproc /usr/sbin/unbound
			exit 0
		fi

		# Update configuration files
		write_tuning_conf
		write_hosts_conf
		write_forward_conf

		boot_mesg "Starting Unbound DNS Proxy..."
		loadproc /usr/sbin/unbound || exit $?

		# Install Safe Search rules when the system is already online
		if [ -e "/var/ipfire/red/active" ]; then
			update_safe_search
		fi
		;;

	stop)
		boot_mesg "Stopping Unbound DNS Proxy..."
		killproc /usr/sbin/unbound
		;;

	restart)
		$0 stop
		sleep 1
		$0 start
		;;
	reload|update-forwarders)
		# Update configuration files
		write_forward_conf
		write_hosts_conf

		# Call unbound-control and perform the reload
		/usr/sbin/unbound-control -q reload

		# Dummy Resolve to wait for unbound
		resolve "ping.ipfire.org" &>/dev/null

		if [ "$1" = "update-forwarders" ]; then
			# Make sure DNS works at this point
			fix_time_if_dns_fails
		fi

		# Update Safe Search rules if the system is online.
		if [ -e "/var/ipfire/red/active" ]; then
			update_safe_search
		fi
		;;

	status)
		statusproc /usr/sbin/unbound
		;;

	resolve)
		resolve "${2}" || exit $?
		;;

	*)
		echo "Usage: $0 {start|stop|restart|reload|status|resolve|update-forwarders}"
		exit 1
		;;
esac
